﻿/*                              
   Copyright 2011 Nils Kopal, Christopher Krüger, Simone Sauer, Dennis Nolte, Uni Duisburg-Essen

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using Sharebert.LinkLayerLibrary.API;
using Sharebert.LinkLayerLibrary;
using Sharebert.OverlayLibrary.API;
using Sharebert.OverlayLibrary.Messages;
using System.Numerics;
using log4net;

namespace Sharebert.OverlayLibrary
{
    /// <summary>
    /// This class represents a connection to another peer in the overlay. It associates a peer connection with its ID
    /// and also manages a clean closure of the connection, when a new peer joins.
    /// </summary>
    public class PeerConnection : IEquatable<PeerConnection>
    {

        private enum ConnectionState
        {
            Default,
            CloseRequested,
            CloseReceived,
            ConnectionEstablished,
            Closed
        }

	private static readonly ILog logger = LogManager.GetLogger(typeof(PeerConnection));
		
        private const int DEFAULT_JOIN_REQUEST_TIMEOUT = 15;


        /// <summary>
        /// Initializes an instance of PeerConnection. This will represent the connection to a peer.
        /// </summary>
        /// <param name="peer">The opposite peer</param>
        /// <param name="connection">The IConnection to the opposite peer</param>
        /// <param name="logger">The logger to use</param>
        public PeerConnection(IPeer peer, IConnection connection)
        {
            this.state = ConnectionState.Default;
            this.Connection = connection;
            this.Connection.ConnectionClosed += new EventHandler<EventArgs>(HandleConnectionClosed);
            this.Connection.ConnectionTimedOut += new EventHandler<EventArgs>(HandleConnectionTimedOut);
            this.Connection.MessageReceived += new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
            this.newConnection = null;
            this.RemotePeer = peer;
            this.closeRequestTimer = new System.Timers.Timer();
            this.closeRequestTimer.AutoReset = false;
            this.closeRequestTimer.Interval = DEFAULT_JOIN_REQUEST_TIMEOUT * 1000;
            this.closeRequestTimer.Elapsed += new System.Timers.ElapsedEventHandler(HandleCloseRequestTimedOut);
        }


        /// <summary>
        /// Sends a request to the opposite peer to close this connection, because of a join operation.
        /// This is used by predecessors to initiate connection closes.
        /// </summary>
        /// <param name="newConnection">The new connection</param>
        public bool RequestCloseOnJoin(PeerConnection newConnection)
        {
            lock (this.lockObj)
            {
                if (this.state != ConnectionState.Default)
                {
                    return false;
                }

                this.state = ConnectionState.CloseRequested;
                this.newConnection = newConnection;
                var localPeerInfo = Serialisation.Serialize(PeerConnection.LocalPeer);
                var header = new Header(MessageTypes.CLOSE_ON_JOIN_REQUEST, localPeerInfo.Length);
                var message = new Message(header, localPeerInfo);
                this.Connection.SendMessage(message);

                this.closeRequestTimer.Start();
                return true;
            }
        }

        /// <summary>
        /// This method is used to signal that a new connection has been established due to join operation.
        /// This is used by successors to permit sending a close confirm message.
        /// </summary>
        public bool NewConnectionEstablished(PeerConnection newConnection)
        {
            lock (this.lockObj)
            {
                if (this.state != ConnectionState.Default && this.state != ConnectionState.CloseReceived)
                {
                    return false;
                }

                this.newConnection = newConnection;
                if (this.state == ConnectionState.CloseReceived)
                {
                    SendCloseOnJoinConfirm();
                }
                else
                {
                    this.state = ConnectionState.ConnectionEstablished;
                    this.closeRequestTimer.Start();
                }
                return true;
            }
        }

        /// <summary>
        /// Two PeerConnections are equal, if they have the same PeerID, listen IP and port. 
        /// (No IConnection relevant parameters are used for the check!)
        /// </summary>
        /// <param name="other">another PeerConnection object</param>
        /// <returns>true if equal</returns>
        public bool Equals(PeerConnection other)
        {
            return this.RemotePeer.Equals(other.RemotePeer);
        }


        #region Connection management and message handling

        private void HandleMessageReceived(object sender, MessageReceivedEventArgs e)
        {
            lock (this.lockObj)
            {
                if (e.Message.Header.Command.Equals(MessageTypes.CLOSE_ON_JOIN_REQUEST))
                {
                    if (this.state == ConnectionState.ConnectionEstablished)
                    {
                        SendCloseOnJoinConfirm();
                    }
                    else
                    {
                        this.state = ConnectionState.CloseReceived;
                        this.closeRequestTimer.Start();
                    }
                }
                else if (e.Message.Header.Command.Equals(MessageTypes.CLOSE_ON_JOIN_CONFIRM))
                {
                    this.closeRequestTimer.Stop();
                    this.state = ConnectionState.Closed;
                    if (this.CloseOnJoinFinished != null)
                    {
                        this.CloseOnJoinFinished.Invoke(this, new PeerConnectionEventArgs(this.newConnection));
                    }
                    logger.Debug(String.Format("Overlay connection to peer {0} successfully closed", this.RemotePeer.ID));
                }
                else if (e.Message.Header.Command.Equals(MessageTypes.PING))
                {
                    var peer = (Peer)Serialisation.Deserialize(e.Message.Payload, typeof(Peer));
                    logger.Debug(String.Format("Received Ping from peer {0}", peer.ID));
                }
                else if (e.Message.Header.Command.Equals(MessageTypes.OFFLINE))
                {
                    var peer = (Peer)Serialisation.Deserialize(e.Message.Payload, typeof(Peer));

                    DeregisterLinkLayerEvents(this.Connection);
                    this.Connection.Close();
                    if (this.PeerIsOffline != null)
                    {
                        this.PeerIsOffline.Invoke(this, new EventArgs());
                    }
                }
            }
        }

        private void HandleConnectionClosed(object sender, EventArgs e)
        {
            lock (this.lockObj)
            {
                DeregisterLinkLayerEvents(this.Connection);
                this.Connection.Close();
                if (this.ConnectionClosed != null)
                {
                    this.ConnectionClosed.Invoke(this, e);
                }
            }
        }

        private void HandleConnectionTimedOut(object sender, EventArgs e)
        {
            lock (this.lockObj)
            {
                DeregisterLinkLayerEvents(this.Connection);
                this.Connection.Close();
                if (this.ConnectionTimedOut != null)
                {
                    this.ConnectionTimedOut.Invoke(this, e);
                }
            }
        }

        private void DeregisterLinkLayerEvents(IConnection conn)
        {
            conn.ConnectionClosed -= new EventHandler<EventArgs>(HandleConnectionClosed);
            conn.ConnectionTimedOut -= new EventHandler<EventArgs>(HandleConnectionTimedOut);
            conn.MessageReceived -= new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
        }

        #endregion


        #region Perform CloseOnJoin

        private void SendCloseOnJoinConfirm()
        {
            try
            {
                var payload = new byte[0];
                var header = new Header(MessageTypes.CLOSE_ON_JOIN_CONFIRM, payload.Length);
                var message = new Message(header, payload);
                this.Connection.SendMessage(message);
                this.state = ConnectionState.Closed;
                if (this.closeRequestTimer.Enabled)
                {
                    this.closeRequestTimer.Stop();
                }
            }
            catch (NetworkException ex)
            {
                logger.Error(String.Format("Could not confirm CloseOnJoin!"), ex);
            }
            finally
            {
                if (this.CloseOnJoinFinished != null)
                {
                    this.CloseOnJoinFinished.Invoke(this, new PeerConnectionEventArgs(this.newConnection));
                }
                DeregisterLinkLayerEvents(this.Connection);
                this.Connection.Close();
                logger.Debug(String.Format("Overlay connection to peer {0} successfully closed", this.RemotePeer.ID));
            }
        }

        private void HandleCloseRequestTimedOut(object sender, System.Timers.ElapsedEventArgs e)
        {
            lock (this.lockObj)
            {
                string newRemoteIp = "-";
                string newRemotePort = "-";
                string newRemoteId = "n/a";
                if(this.newConnection != null)
                {
                    if(this.newConnection.Connection != null)
                    {
                        newRemoteIp = this.newConnection.Connection.RemoteAddress;
                        newRemotePort = this.newConnection.Connection.RemotePort.ToString();
                    }
                    if(this.newConnection.RemotePeer != null)
                    {
                        newRemoteId = this.newConnection.RemotePeer.ID.ToString();
                    }
                }
                logger.Debug(String.Format("CloseRequestTimer on connection to peer {0} timed out. Dropping join request of peer {1}:{2} ({3})!", 
                    this.RemotePeer.ID, newRemoteIp, newRemotePort, newRemoteId));
                this.newConnection.Connection.Close();
                this.newConnection = null;
                this.state = ConnectionState.Default;
            }
        }

        #endregion


        #region Properties and private members

        public IConnection Connection { get; private set; }

        public IPeer RemotePeer { get; private set; }

        public static IPeer LocalPeer { get; set; }

        private PeerConnection newConnection;

        private ConnectionState state;

        private System.Timers.Timer closeRequestTimer;

        private object lockObj = new object();

        #endregion


        #region Events

        public event EventHandler<PeerConnectionEventArgs> CloseOnJoinFinished;

        public event EventHandler<EventArgs> ConnectionClosed;

        public event EventHandler<EventArgs> ConnectionTimedOut;

        public event EventHandler<EventArgs> PeerIsOffline;

        #endregion

    }
}
